-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Promsafe: Strongly-typed safe labels #1598
base: main
Are you sure you want to change the base?
Conversation
14250ae
to
b783c16
Compare
011822d
to
80149ab
Compare
Hi! Thanks for innovating here 💪🏽 I presume this is about using generics for label values type safety -- in the relation to defined label names.
Can you share exactly the requirements behind For example, how often you see those Generally, what's recommended is hardcoding label values in Thus, let's circle back to barebone requirements we want here 🤗 e.g. generally you should avoid using Additionally, performance is important for this increment flow, so it would be nice to check how this applies. |
Hey. Thanks for a feedback. Let me share details on my motivation behind the provided By // Counter registration: we're fine with possible panic here :)
myCounter := promauto.NewCounterVec(prometheus.CounterOpts{
Name: "items_counted",
}, []string{"event_type", "success", "slot" /* 1/2/3/.../9 */})
// But using counter: where there motivation comes from:
// Using .GetMetricWith*() methods will error if labels are messed up
myCounterWithValues, err := myCounter.GetMetricWith(prometheus.Labels{
"event_type": "reservation",
"success": "true",
"slot": "1",
})
if err != nil {
// TODO: handle error
}
// Same error can happen if using *WithLabelValues version:
// myCounterWithValues, err := myCounter.GetMetricWithLabelValues("reservation", "true", "1")
// To avoid error-handling we can use .With/.WithLabelValues, but it will just panic for the same reasons:
myCounter.WithLabelValues("reservation", "true", "2").Inc() 💡 So here and further i call "panic" both panicing of Why Panic? why it matters?Here are several reasons:
All these reasons are possible ways to break code because of panicking in .With() or .WithLabelValues(). Let's not spend time and efforts on code-reviews to ensure that new usage of "counter inc" is not breaking everything. Also, one more reason is not about failing but about consistency: How it's solved by promsafe?// Promsafe example:
// Registering a metric with simply providing the type containing labels
type MyCounterLabels struct {
promsafe.StructLabelProvider
EventType string
Success bool
Slot uint8 // yes, it's a number, still should be careful with high-cardinality labels
}
myCounterSafe := promsafe.NewCounterVec[MyCounterLabels](prometheus.CounterOpts{
Name: "items_counted_detailed",
})
// Calling counter is simple: just provide the filled struct of the dedicated type.
//
// Neither of 5 reasons can panic here. You simply can't mess up the struct.
// With() accepts ONLY this type of struct, you can't send any other struct.
// You don't need to remember the fields and their order. IDE will show you them.
// You can't send more fields.
// You can send less fields (but it can easily fill up with default values, or other custom non-panicy logic).
// You can't mess up types.
// You're consistent with types.
myCounterSafe.With(MyCounterLabels{
EventType: "reservation", Success: true, Slot: 1,
}).Inc() P.S. issue with inconsistency of promsafe-version of WithLabelValues() method// One thing that I need to specify here is the inconsistency with promsafe-version of WithLabelValues()
// WithLabelValues() excepts ordered raw strings, that unfortunately breaks the "safety" concept.
// We can't control the order of given strings and even the length of it
// That's why in promsafe, .WithLavelValues() and .GetMetricWithLabelValues() are disabled:
// They are marked deprecated and panic (so they are strongly considered not to be used) |
This API is really nice, would love to see this merged. I've already ran into a few of the failure modes @amberpixels mentioned in my first few weeks of using this library. |
Small update. I've pushed some improvements in API, so it's more consistent and stable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for amazing work!
I think this is a great direction, but I'm not sure it's at the stage where we want to claim full stability and maintenance of it in the client_golang v1.
I would like to explore slimmer "adapter" that just offers With(labels T) K
method -- it would simplify the code to maintain and allow composability.
Then there is efficiency aspect I would like to understand, given this is a hot path.
Also before committing to any of this we have to ask ourselves what to recommend or deprecate in this place. We are getting to the place where there are many ways of doing the same thing, so would love to decide what to remove, if we think this is the way to go.
To achieve and answer all of this, I wonder:
A) How bad would it be to host promsafe
in another repository for incubation period?
B) Is there a room for prometheus/client_golang/exp
module which we could version v0.x and put other experimental stuff like Remote API?
// limitations under the License. | ||
|
||
// Package promauto_adapter provides compatibility adapter for migration of calls of promauto into promsafe | ||
package promauto_adapter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put it in promsafe
honestly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, the promauto_adapter
idea is very weak.
But the issue does exist - my intention was to support both APIs: the prometheus
and promauto
. So if you had either prometheus.NewCounterVec
or promauto.NewCounterVec
- you can easily switch to promsafe.NewCounterVec
(using named import
in case of promauto_adapter)
The other approach is - if we want to keep both, and keep them in one package, is to give them different names e.g. promsafe.NewCounterVec
makes a Typed version of prometheus.NewCounterVec
, but promsafe.NewAutoCounterVec
makes a Typed version of promauto.NewCounterVec
201bb52
to
7a3bcd9
Compare
There are benchmarks in
Using automatic label extraction (via reflection) results in a 2x performance overhead compared to simple
I completely agree with this.
I like the idea of a
Understood. For a minimal implementation, I see the following setup: So are you ok with such "slim" adapter? (it will be required to be implemented per metric type) // NewCounterVec creates a new CounterVec with type-safe labels.
func NewCounterVec[T LabelsProviderMarker](opts prometheus.CounterOpts) *CounterVec[T] {
emptyLabels := NewEmptyLabels[T]()
inner := prometheus.NewCounterVec(opts, extractLabelNames(emptyLabels))
return &CounterVec[T]{inner: inner}
}
// CounterVec is a wrapper around prometheus.CounterVec that allows type-safe labels.
type CounterVec[T LabelsProviderMarker] struct {
inner *prometheus.CounterVec
}
// With behaves like prometheus.CounterVec.With but with type-safe labels.
func (c *CounterVec[T]) With(labels T) prometheus.Counter {
return c.inner.With(extractLabelsWithValues(labels))
} |
55582e1
to
7828200
Compare
…quired) Signed-off-by: Eugene <[email protected]>
7828200
to
879c474
Compare
Promsafe
Introducing
promsafe
lib (optional helper lib, similar topromauto
) that allows to use type-safe labels.Motivation
This PR only covers
Counter
functionality as an example. If idea is fine for community, I'll push further commits expanding logic toGauge
,Histogram
, etcFor detailed motivation see my comment below
Fixes #1599
Why?
Currently having unsafe labels lead to several problems: either err-handling nightmare, either panicing (in case if you use "promauto")
Having unsafe labels can lead to following issues:
As of state of art of modern Go version, we can use Go Generics to solve these issues.
Examples of how to use it
1. Multi-label mode (safe structs)
Compatibility with
promauto
1.
promauto.With
call migrationNote:
All non-string value types will be automatically converted to string. Here we can add a reasonable type-validation, so we can make it only to work with fields that are strings/bools/ints